package me.cabbit.digitone.tones.halftone;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.DataBufferByte;

import me.cabbit.digitone.tones.Screentone;

import org.w3c.dom.Element;

/**
 * A screentone that takes an image and creates a dotted pattern from it
 * 
 * Special thanks to Krechevskoy for the idea. Making other screentones
 * work from this one simplified things quite some! :D
 * 
 * @author Arkaid
 *
 */
public abstract class Halftone extends Screentone {
	
	//------------------------------------------------------------------//
	//--------------------------- properties ---------------------------//
	//------------------------------------------------------------------//

	/** Maximum dot size */
	private int maxSize;
	
	/** Minimim dot size */
	private int minSize;

	/** horizontal space between dots */
	private int hSpacing;

	/** vertical space between dots */
	private int vSpacing;
	
	/** Angle of the screentone's rows */
	private int angle;

	/** whether to invert colors or not */
	private boolean invertColors;

	/** Source image to process */
	BufferedImage source;
	
	//------------------------------------------------------------------//
	//----------------------- setters and getters ----------------------//
	//------------------------------------------------------------------//
	
	/** Sets the biggest possible size for a dot */
	public void setMaxSize (int maxSize)
	{
		this.maxSize = maxSize;
	}
	
	/** Gets the biggest possible size for a dot */
	public int getMaxSize ()
	{
		return this.maxSize;
	}
	
	/** Sets the smallest possible size for a dot */
	public void setMinSize (int minSize)
	{
		this.minSize = minSize;
	}
	
	/** Gets the smallest possible size for a dot */
	public int getMinSize ()
	{
		return this.minSize;
	}
	
	/** Sets the horizontal space between dots */
	public void setHorizontalSpacing(int hs) 
	{ 
		this.hSpacing = hs;	
	}

	/** Sets the horizontal space between dots */
	public int getHorizontalSpacing() 
	{ 
		return this.hSpacing;
	}
	
	/** Sets the vertical space between dots */
	public void setVerticalSpacing(int vs) 
	{ 
		this.vSpacing = vs; 
	}
	
	/** Gets the horizontal space between dots */
	public int getVerticalSpacing() 
	{ 
		return this.vSpacing; 
	}
	
	/** Sets the angle of rotation for the dots
	 * Anything other than 0~90 gets clamped */
	public void setAngle(int angle) 
	{
		//make sure the angle stays between 0 and 90
		this.angle = Math.min(Math.max(angle, 0), 90); 
	}

	/** Gets the angle of rotation for the dots */
	public int getAngle() 
	{ 
		return this.angle; 
	}
	
	/** Set to true to invert black and white */
	public void setInvertColors(boolean ic) 
	{ 
		this.invertColors = ic; 
	}

	/** Get if colors are inverted */
	public boolean getInvertColors() { return this.invertColors; }
	
	/** Sets the image to use for the halftone process */
	public void setSource(BufferedImage img)
	{
		this.source = img;
	}
	
	//------------------------------------------------------------------//
	//----------------------------- methods ----------------------------//
	//------------------------------------------------------------------//

	/** Constructor. Sets default values */
	public Halftone() 
	{
		this.minSize = 0;
		this.maxSize = 6;
		this.hSpacing = 5;
		this.vSpacing = 5;
		this.angle = 35;
		this.invertColors = false;
	}
	
	@Override
	public void render(Graphics2D target) {

		//invert color check
		Color dotColor = this.invertColors ? Color.WHITE : Color.BLACK;
		Color bgColor = this.invertColors ? Color.BLACK : Color.WHITE;

		//fill out background
		target.setColor(bgColor);
		target.fillRect(0, 0, this.getWidth(), this.getHeight());
		target.setColor(dotColor);
		
		// Make certain the image is greyscale
        ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
        BufferedImage grayImg = new ColorConvertOp(cs, null).filter(this.source, null);
		
        //scale to fit canvas size (keeping aspect ratio)
        BufferedImage scaled;
        double aspect = (double)grayImg.getWidth() / (double)grayImg.getHeight();
        
        int tx, ty;		//to center the image inside the canvas
        int imgW = this.getWidth();
        int imgH = this.getHeight();

        //compare aspect ratios and scale accordingly
    	if (aspect < (imgW / (double)imgH))
    	{
    		//canvas is wider than image so
    		//height fits to canvas height
    		//and width scales
        	imgW = (int) (this.getHeight() * aspect);
        	tx = (this.getWidth() - imgW) / 2;
        	ty = 0;
    	}
    	else
    	{
    		//the other way around
    		imgH = (int) (this.getWidth() / aspect);
        	tx = 0;
        	ty = (this.getHeight() - imgH) / 2; 
        }
		
    	scaled = new BufferedImage(imgW, imgH, BufferedImage.TYPE_BYTE_GRAY);
        scaled.createGraphics().drawImage(grayImg, 0, 0, imgW, imgH, 0, 0, grayImg.getWidth(), grayImg.getHeight(), null);
        
        //obtain an array of bytes (faster than getRGB per each pixel)
        byte [] pixels = ((DataBufferByte) scaled.getRaster().getDataBuffer()).getData();

        //since rotating the canvas could produce empty spaces
        //around the corners, draw dots well beyond the size
        //of the canvas prior to rotation. If the rotated dot
        //falls outside the canvas, do not draw it
        int w = this.getWidth();
		int h = this.getHeight();
		
		//the largest possible square is determined by the diagonal
		//of the canvas
        int diagonal = (int)Math.sqrt(w*w+h*h);
        w = diagonal * 2;
        h = diagonal;
		
		//angle to radians. Precalculate sin and cos
		double radAngle = this.angle / 180.0 * Math.PI;
		double cosAngle = Math.cos(radAngle);
		double sinAngle = Math.sin(radAngle);
		
		
		//begin them dots
		int xStep = (this.minSize / 2) + this.hSpacing;
		int yStep = (this.minSize / 2) + this.vSpacing;
		
		//calculate the total needed size for progress
		int totalRows = (2 * h) / yStep;
		int currentRow = 0;

		for (int j = -h; j <= h; j+= yStep)
		{
			for (int i = 0; i <= w; i+= xStep)
			{
				//calculate x and y post rotation
				int x = (int)(i * cosAngle - j * sinAngle);
				int y = (int)(i * sinAngle + j * cosAngle);
				
				//falls out of the image size
				if (x < 0 || x >= imgW || y < 0 || y >= imgH)
					continue;
				
				//calculate size of the dot.
				//it will be a function of the gray color of the
				//image in the same place
				int size = calculateDotSize(x, y, pixels, scaled.getWidth(), xStep, yStep);
				
				//magic~
				target.fillOval(tx + x, ty + y, size, size);
			}

			//increase row and calculate progress
			int progress = (int)(((double)currentRow / totalRows) * 100);
			raiseBuildProgressChangedEvent(progress);
			currentRow++;
		}

		//the rest :3
		raiseBuildProgressChangedEvent(100);
	}

	/**
	 * Calculates the size of a dot given a particular position
	 * in the image. The average color in the area to be replaced
	 * by the dot determines the dot size
	 * 
	 * @param x horizontal coordinate of the pixel to replace
	 * @param y vertical coordinate of the pixel to replace
	 * @param pixels array with pixel data
	 * @param stride width of the image
	 * @param xStep width of the area to be replaced (maxDotSize + hSpacing)
	 * @param yStep height of the area to be replaced (maxDotSize + vSpacing)
	 * @return size of the dot
	 */
	private int calculateDotSize(int x, int y, byte [] pixels, int stride, int xStep, int yStep)
	{
		//average color (gray)
		int avg = 0;
		
		//number of sampled pixels
		int nSamples = 0;
		
		//calculate the average of grays for the
		//area we're going to replace with a dot
		for(int i = 0; i < xStep; i++)
		{
			for(int j = 0;  j < yStep; j++)
			{
				int index = (y + j) * stride + x + i;
				
				if (index >= pixels.length) 
					continue;
				
				avg += (int)(pixels[index] & 0xff);
				nSamples++;
			}
		}

		int gray = nSamples > 0 ? 255 - avg / nSamples : 255;

		//the size of the dot is linearly related to the gray value
		return (int) ((this.maxSize - this.minSize) * (gray / 255.0) + this.minSize);
	}
	
	@Override
	public void loadPreset(Element node) 
	{
		this.minSize = Integer.parseInt(node.getAttribute("minSize"));
		this.maxSize = Integer.parseInt(node.getAttribute("maxSize"));
		this.hSpacing = Integer.parseInt(node.getAttribute("hSpacing"));
		this.vSpacing = Integer.parseInt(node.getAttribute("vSpacing"));
		this.angle = Integer.parseInt(node.getAttribute("angle"));
		this.invertColors = Boolean.parseBoolean(node.getAttribute("invert"));
	}

	@Override
	public void savePreset(Element node) 
	{
		node.setAttribute("minSize", String.valueOf(this.minSize));
		node.setAttribute("maxSize", String.valueOf(this.maxSize));
		node.setAttribute("hSpacing", String.valueOf(this.hSpacing));
		node.setAttribute("vSpacing", String.valueOf(this.vSpacing));
		node.setAttribute("angle", String.valueOf(this.angle));
		node.setAttribute("invert", String.valueOf(this.invertColors));
	}

	
}
